iT邦幫忙

2024 iThome 鐵人賽

DAY 6
0
Mobile Development

從概念發想上架一支SwiftUI app系列 第 6

基礎部分 - 重構程式碼, 實作光束效果

  • 分享至 

  • xImage
  •  

今天拆分成不同檔案,建立LightSettings存放操作變數。

LightSettings.swift

import SwiftUI
struct LightSettings {
    var circleSize: CGFloat
    var brightness: Double = 1.0
    var color: Color
    var offset: CGSize
    var rotationOffset: CGSize
    
    func getAngelX() -> CGFloat {
        return -1 * atan2(self.rotationOffset.width, 75) * 360 / .pi
    }
    
    func getAngelY() -> CGFloat {
        return atan2(self.rotationOffset.height, 75) * 360 / .pi
    }
}

ContentView.swift

import SwiftUI

struct ContentView: View {
    @State private var lightSettings = LightSettings(
        circleSize:CGFloat(50),
        brightness: 1.0,
        color: .white,
        offset: .zero,
        rotationOffset: .zero
    )
    
    var body: some View {
        VStack {
            LightZone(lightSettings: lightSettings)
                .frame(maxWidth: .infinity)
            PanelView(lightSettings: $lightSettings)
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

LightZone.swift
光線效果嘗試了好幾個做法,最後發現RadialGradient最適合模擬光源效果的邊緣漸層。
rotation3DEffect能滿足我要的需求,個別對3軸做出變形效果。
還想要改善大角度時的光束效果,應該要有一個Slider可設定光束長短,下階段安排進清單。

import SwiftUI

struct LightZone: View {
    let scale = UIScreen.main.bounds.width/200.0
    let lightSettings: LightSettings
    let blurRate = 0.2
    var body: some View {
        ZStack {
            Color.black
                .ignoresSafeArea()

            Circle()
                .fill(
                    RadialGradient(
                        gradient: Gradient(colors: [lightSettings.color.opacity(lightSettings.brightness), .clear]),
                        center: .center,
                        startRadius: lightSettings.circleSize*0.15,
                        endRadius: lightSettings.circleSize*0.5
                    )
                )
                .frame(width: lightSettings.circleSize, height: lightSettings.circleSize)
                .rotation3DEffect(.degrees(lightSettings.getAngelY()),axis: (x: 1, y: 0, z: 0))
                .rotation3DEffect(.degrees(lightSettings.getAngelX()), axis: (x: 0, y: 1, z: 0))
                .offset(CGSize(width: lightSettings.offset.width*scale, height: lightSettings.offset.height*scale))
        }
    }
}

*PanelView.swift *
這裡還有x,y軸定義混亂的問題待修正(考量截稿時間優先)
轉動角度應該控制在60度以內就夠了。

import SwiftUI

struct PanelView: View {
    
    @Binding var lightSettings: LightSettings
    
    var body: some View {
        VStack {
            Slider(value: $lightSettings.circleSize, in: 20...80)
            Text("光圈大小: \(Int(lightSettings.circleSize))")
            Slider(value: $lightSettings.brightness, in: 0.5...1)
            Text("亮度: \(lightSettings.brightness, format: .percent)")
            ColorPicker("顏色選擇", selection: $lightSettings.color)
            Text("角度: (\(-1*lightSettings.getAngelX(), specifier: "%.2f"), \(-1*lightSettings.getAngelY(), specifier: "%.2f"))")
            HStack {
                RotationPanel(rotationOffset: $lightSettings.rotationOffset)
                                    .frame(width: 150, height: 150)
                PositionPanel(offset: $lightSettings.offset)
                    .frame(width: 200, height: 200)
            }
        }
        .padding()
    }
}

struct PositionPanel: View {
    @Binding var offset: CGSize
    @State private var lastOffset: CGSize = .zero

    var body: some View {
        ZStack {
            Rectangle()
                .stroke(Color.green, lineWidth: 2)
                
            MovableCircle()
                .offset(offset)
                .gesture(
                    DragGesture()
                        .onChanged { value in
                            let maxOffset = 100.0
                            let newX = max(-maxOffset, min(value.translation.width + lastOffset.width, maxOffset))
                            let newY = max(-maxOffset, min(value.translation.height + lastOffset.height, maxOffset))
                            offset = CGSize(width: newX, height: newY)
                        }
                        .onEnded { _ in
                            lastOffset = offset
                        }
                )
        }
    }
}

struct RotationPanel: View {
    @Binding var rotationOffset: CGSize
    @State private var lastOffset: CGSize = .zero

    var body: some View {
        GeometryReader { geometry in
            ZStack {
                Circle()
                    .stroke(Color.gray, lineWidth: 2)
                    .frame(width: 150, height: 150)
                Circle()
                    .fill(Color.blue)
                    .frame(width: 20, height: 20)
                    .offset(rotationOffset)
                    .gesture(
                        DragGesture()
                            .onChanged { value in
                                let radius = 75.0 // 圓形的半徑
                                var newX = value.translation.width + lastOffset.width
                                var newY = value.translation.height + lastOffset.height

                                let distance = sqrt(newX * newX + newY * newY)
                                if distance > radius {
                                    let angle = Double(atan2(newY, newX))
                                    newX = radius * cos(angle)
                                    newY = radius * sin(angle)
                                }

                                rotationOffset = CGSize(width: newX, height: newY)
                            }
                            .onEnded { _ in
                                lastOffset = rotationOffset
                            }
                )
            }
        }
    }
}

struct MovableCircle: View {
    var body: some View {
        Circle()
            .fill(Color.red)
            .frame(width: 20, height: 20)
    }
}

看看模擬器效果如何
DAY6
第一階段試驗告一段落,下一步是盤點上架之前要加入的小功能。

  • PositionPanel加個小按鈕快速置中
  • RotationPanel加一個Lock按鈕,給user決定手勢結束後要不要自動朝上
  • 加個開關給user決定螢幕自動調到最亮
  • 支援多尺寸,這是一個好練習。

趁著週末有滿滿的時間,我會先寫一篇關於元件Layout的題材。
APP要支援不同尺寸裝置,以及直向與橫向。
一轉眼已經Day6,期許接下來內容更豐富一些。
摸索SwiftUI一段,多虧新的AI工具Gemini加速學習過程,繼續做下去,迎接新挑戰吧!


上一篇
基礎部分 - 重構討論
下一篇
基礎部分 - 調整PanelView排版
系列文
從概念發想上架一支SwiftUI app30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言